local integers = {}
local integerNumBytes = {}
local tuples = {}
local rawStrings = {}
local rawTableLength = {}
local function clearSerializationData()
	integers = {}
	integerNumBytes = {}
	tuples = {}
	rawStrings = {}
	rawTableLength = {}
end

function CurseProfiler.makeSerializedString(str)
	if (str == nil) then
		return "";
	else
		return tostring(str);
	end
end
CurseProfiler.STRING = CurseProfiler.makeSerializedString;

function CurseProfiler.makeSerializedInteger(kind, integer)
	local min, max, numBytes
	if kind == "Byte" then
		min = 0
		max = 2^8 - 1
		numBytes = 1
	elseif kind == "Int16" then
		min = -2^15
		max = 2^15 - 1
		numBytes = 2
	elseif kind == "UInt16" then
		min = 0
		max = 2^16 - 1
		numBytes = 2
	elseif kind == "Int32" then
		min = -2^31
		max = 2^31 - 1
		numBytes = 4
	elseif kind == "Int64" then
		min = -2^63
		max = 2^63 - 1
		numBytes = 8
	else
		error(("Bad integer type %s"):format(tostring(kind)), 2)
	end
	local p = newproxy()
	if (integer == nil) then
		integer = 0;
	elseif type(integer) == "boolean" then
		if integer then
			integer = 1;
		else
			integer = 0;
		end
	elseif type(integer) ~= "number" then
		error(("Bad type %s (%s)"):format(tostring(type(integer)), tostring(integer)), 2)
	elseif math.floor(integer) ~= integer then
		error(("%s is not an integer."):format(tostring(integer)), 2)
	end    
    if integer <= min-1 then
        integer = min
    end    
	if integer >= max+1 then
		error(("%s does not fit into a %s [%s, %s]"):format(integer, kind, min, max), 2)
	end
	integers[p] = integer
	integerNumBytes[p] = numBytes
	return p
end
local INT = CurseProfiler.makeSerializedInteger;
CurseProfiler.INT = INT;


function CurseProfiler.makeSerializedTuple(tab)
	local p = newproxy()
	tuples[p] = tab
	return p
end
local TUPLE = CurseProfiler.makeSerializedTuple;
CurseProfiler.TUPLE = TUPLE;


local function getDataType(data)
	local type_data = type(data)
	if type_data == "userdata" then
	 	if integers[data] then
			return "integer"
		elseif tuples[data] then
			return "tuple-" .. getDataType(tuples[data])
		elseif rawStrings[data] then
			return "raw"
		end
	elseif type_data ~= "table" then
		return type_data
	end
	local n = #data
	local isList = true
	local isSet = true
	for k, v in pairs(data) do
		if isSet and v ~= true then
			isSet = false
		end
		if isList then
			if type(k) ~= "number" or k < 1 or k > n then
				isList = false
			end
		end
		if not isList and not isSet then
			break
		end
	end	
	if isSet then
		return "set"
	elseif isList then
		for i = 1, n do
			if data[i] == nil then
				return "dict"
			end
		end
		return "list"
	else
		return "dict"
	end
	error("Unknown data type", 2);
end


local function getTableSize(data)
	data = tuples[data] or data
	local dataType = getDataType(data)
	if dataType == "set" then
		local num = 0
		for k in pairs(data) do
			local dataType = getDataType(k)
			if rawTableLength[k] then
				num = num + rawTableLength[k]
			else
				num = num + 1
			end
		end
		return num
	elseif dataType == "list" then
		local num = 0
		for i, v in ipairs(data) do
			if rawTableLength[v] then
				num = num + rawTableLength[v]
			else
				num = num + 1
			end
		end
		return num
	elseif dataType == "dict" then
		local num = 0
		for k, v in pairs(data) do
			if rawTableLength[v] then
				num = num + rawTableLength[v]
			else
				num = num + 1
			end
		end
		return num
	end
	return rawTableLength[data] or 1
end

local function serialize(data, t)
	local madeT = not t
	local isTuple = tuples[data]
	if isTuple then
		data = isTuple
	end
	local dataType = getDataType(data)
	if madeT then
		t = {}
	end
	if dataType == "raw" then
		t[#t+1] = rawStrings[data]
	elseif dataType == "integer" then
		local num = integers[data]
        
		for i = 1, integerNumBytes[data] do
			local c = num % 256
            if c < 0 then
                c = c + 256
            end            
			t[#t+1] = string.char(c)
			num = (num - c) / 256
		end
	elseif dataType == "boolean" then
		t[#t+1] = data and string.char(1) or string.char(0)
	elseif dataType == "string" then
		serialize(INT("UInt16", #data), t)
		t[#t+1] = data
	elseif dataType == "list" then
		if not isTuple then
			serialize(INT("UInt16", getTableSize(data)), t)
		end
		for i, v in ipairs(data) do
			serialize(v, t)
		end
	elseif dataType == "set" then
		if not isTuple then
			serialize(INT("UInt16", getTableSize(data)), t)
		end
		local keys = {}
		for k in pairs(data) do
			keys[#keys+1] = k
		end
		--table.sort(keys, set_sort)
		for _, k in ipairs(keys) do
			serialize(k, t)
		end
	elseif dataType == "dict" then
		if not isTuple then
			serialize(INT("UInt16", getTableSize(data)), t)
		end
		local keys = {}
		for k in pairs(data) do
			keys[#keys+1] = k
		end
		--table.sort(keys, set_sort)
		for _, k in ipairs(keys) do
			local v = data[k]
			serialize(k, t)
			serialize(v, t)
		end
	else
		error(("Unknown data type: %q (%s)"):format(dataType, tostring(data)), 2)
	end
	if madeT then
		return table.concat(t)
	end
end
CurseProfiler.serialize = serialize;



local base64encode, base64decode
do
	local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
	local A_byte = ("A"):byte()
	local Z_byte = ("Z"):byte()
	local a_byte = ("a"):byte()
	local z_byte = ("z"):byte()
	local zero_byte = ("0"):byte()
	local nine_byte = ("9"):byte()
	local plus_byte = ("+"):byte()
	local slash_byte = ("/"):byte()
	local function getNum(byte)
		if byte >= A_byte and byte <= Z_byte then
			return byte - A_byte
		elseif byte >= a_byte and byte <= z_byte then
			return byte - a_byte + 26
		elseif byte >= zero_byte and byte <= nine_byte then
			return byte - zero_byte + 52
		elseif byte == plus_byte then
			return 62
		elseif byte == slash_byte then
			return 63
		end
	end
	local t = {}
	function base64encode(text)
		for i = 1, #text, 3 do
			local a, b, c = text:byte(i, i+2)
			local nilNum = 0
			if not b then
				nilNum = 2
				b = 0
				c = 0
			elseif not c then
				nilNum = 1
				c = 0
			end
			local num = a * 2^16 + b * 2^8 + c
			
			local d = num % 2^6
			num = (num - d) / 2^6
			
			local c = num % 2^6
			num = (num - c) / 2^6
			
			local b = num % 2^6
			num = (num - b) / 2^6
			
			local a = num % 2^6
			
			t[#t+1] = chars:sub(a+1, a+1)
			t[#t+1] = chars:sub(b+1, b+1)
			if nilNum >= 2 then
				t[#t+1] = "="
			else
				t[#t+1] = chars:sub(c+1, c+1)
			end
			if nilNum >= 1 then
				t[#t+1] = "="
			else
				t[#t+1] = chars:sub(d+1, d+1)
			end
		end
		local s = table.concat(t)
		for i = 1, #t do
			t[i] = nil
		end
		return s
	end
	function base64decode(text)
		for i = 1, #text, 4 do
			local a, b, c, d = text:byte(i, i+3)
			a, b, c, d = getNum(a), getNum(b), getNum(c), getNum(d)
			
			local nilNum = 0
			if not c then
				nilNum = 2
				c = 0
				d = 0
			elseif not d then
				nilNum = 1
				d = 0
			end
			
			local num = a * 2^18 + b * 2^12 + c * 2^6 + d
			
			local c = num % 2^8
			num = (num - c) / 2^8
			
			local b = num % 2^8
			num = (num - b) / 2^8
			
			local a = num % 2^8
			num = (num - a) / 2^8
			
			t[#t+1] = string.char(a)
			if nilNum < 2 then
				t[#t+1] = string.char(b)
			end
			if nilNum < 1 then
				t[#t+1] = string.char(c)
			end
		end
		local s = table.concat(t)
		for i = 1, #t do
			t[i] = nil
		end
		return s
	end
end
CurseProfiler.base64encode = base64encode;